package com.google.code.gronono.commons.image;

import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.GraphicsEnvironment;
import java.awt.Image;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;

import javax.imageio.ImageIO;
import javax.imageio.ImageWriter;
import javax.imageio.stream.ImageOutputStream;
import javax.imageio.stream.MemoryCacheImageOutputStream;

import org.apache.log4j.Logger;

/**
 * Classe implémentant des méthodes utilitaires pour les images.
 * <br/>Code grandement inspiré de <a href="https://jveez.dev.java.net/source/browse/jveez/src/net/java/jveez/utils/ImageUtils.java?hideattic=0&rev=1.11&view=markup">net.java.jveez.utils.ImageUtils</a>.
 *
 * @author gael.lorent-fonfrede
 */
public class ImageUtils {
	/** Logger. */
	private static final Logger logger = Logger.getLogger(ImageUtils.class);

	/** Configuration graphique. */
	private final static GraphicsConfiguration graphicsConfiguration = GraphicsEnvironment.getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration();

	/**
	 * Cette classe est une classe utilitaire.
	 * Son constructeur ne doit jamais être appelé.
	 * @throws UnsupportedOperationException Toujours.
	 */
	private ImageUtils() {
		throw new UnsupportedOperationException();
	}

	/**
	 * Charger une {@link BufferedImage} à partir d'un flux d'octets.
	 *
	 * @param imageData Le flux d'octets.
	 * @return L'image.
	 *
	 * @see #getImageBytes(BufferedImage, String) pour l'opération inverse.
	 */
	public static BufferedImage loadImage(final byte[] imageData) {
		try {
			final BufferedImage bufferedImage = ImageIO.read(new ByteArrayInputStream(imageData));
			// here, we will copy the (unmanaged) image from ImageIO into a compatible (managed) image to ensure best performance
			final BufferedImage image = copyIntoCompatibleImage(bufferedImage, bufferedImage.getWidth(), bufferedImage.getHeight());
			return image;
	    } catch (final Throwable t) {
			final StringBuilder msg = new StringBuilder("Throwable: could not load the image: ").append(t.getMessage());
			if (t.getCause() != null) msg.append(t.getCause());
			logger.error(msg.toString(), t);
			return null;
	    }
	}

	/**
	 * Rend une {@link Image} compatible avec une résolution.
	 *
	 * @param image L'image source.
	 * @param width La largeur.
	 * @param height La hauteur.
	 * @return L'image modifiée.
	 */
	public static BufferedImage copyIntoCompatibleImage(final Image image, final int width, final int height) {
	    if (image == null) return null;
	    final BufferedImage compatibleImage = graphicsConfiguration.createCompatibleImage(width, height);
	    final Graphics2D g2d = compatibleImage.createGraphics();
	    g2d.drawImage(image, 0, 0, null);
	    g2d.dispose();
	    return compatibleImage;
	}

	/**
	 * Charge un flux d'octets à partir d'une {@link BufferedImage}.
	 *
	 * @param bufferedImage L'image source.
	 * @param mimeType Le type mime.
	 * @return Le flux d'octets.
	 *
	 * @see #loadImage(byte[]) pour la transformation inverse.
	 */
	public static byte[] getImageBytes(final BufferedImage bufferedImage, final String mimeType) {
		long start = 0;
		if (logger.isDebugEnabled()) start = System.currentTimeMillis();
	    final ImageWriter writer = ImageIO.getImageWritersByMIMEType(mimeType).next();
	    final ByteArrayOutputStream bos = new ByteArrayOutputStream();
	    final ImageOutputStream output = new MemoryCacheImageOutputStream(bos);
		try {
			writer.setOutput(output);
			writer.write(bufferedImage);
			if (logger.isDebugEnabled()) logger.debug("image stored in " + (System.currentTimeMillis() - start) + "ms");
	    } catch (final Exception e) {
			final StringBuilder msg = new StringBuilder("Exception: An exception has been caught while saving image as a byte array: ").append(e.getMessage());
			if (e.getCause() != null) msg.append(e.getCause());
			logger.error(msg, e);
			return null;
	    } finally {
			close(output);
	    }
		return bos.toByteArray();
	}

	/**
	 * Ferme un {@link ImageOutputStream}.
	 *
	 * @param output Le flux à fermer.
	 */
	private static void close(final ImageOutputStream output) {
		if (output != null) {
			try {
				output.close();
			} catch (final IOException e) {
				final StringBuilder msg = new StringBuilder("IOException: Could not close the output stream: ").append(e.getMessage());
				if (e.getCause() != null) msg.append(e.getCause());
				logger.warn(msg, e);
			}
		}
	}

	/**
	 * Transforme une {@link BufferedImage} en appliquant des rotations.
	 *
	 * @param image L'image à transformer.
	 * @param rotate Indique s'il faut appliquer une rotation.
	 * @param clockWise Si <code>true</code>, indique si la rotation est horaire (et inversement <code>false</code> indique une rotation anti-horaire).
	 * @param flipAroundX Indique s'il faut appliquer une rotation sur l'axe des abcisses.
	 * @param flipAroundY Indique s'il faut appliquer une rotation sur l'axe des ordonnées.
	 * @return L'image transformée.
	 */
	public static BufferedImage transformImage(final BufferedImage image,
			boolean rotate, final boolean clockWise, boolean flipAroundX,
			boolean flipAroundY) {
		// Si aucune rotation à appliquer on retourne l'image telle quelle
		if (!rotate && !flipAroundX && !flipAroundY) {
			return image;
		}

		// Détermination des dimensions de l'image cible
		int targetWidth = image.getWidth();
		if (rotate) targetWidth = image.getHeight();
		int targetHeight = image.getHeight();
		if (rotate) targetHeight = image.getWidth();

		final AffineTransform transform = new AffineTransform();

		// Préparation des transformations
		if (flipAroundX) {
			transform.translate(0, targetHeight);
			transform.scale(1, -1);
		}
		if (flipAroundY) {
			transform.translate(targetWidth, 0);
			transform.scale(-1, 1);
		}
		if (rotate) {
			if (clockWise) {
				transform.translate(image.getHeight(), 0);
				transform.rotate(Math.PI / 2);
			} else {
				transform.translate(0, image.getWidth());
				transform.rotate(-Math.PI / 2);
			}
		}

		// Application des transformations, dans une image rendue compatible avec les dimensions cibles
		final BufferedImage copy = graphicsConfiguration.createCompatibleImage(targetWidth, targetHeight);
		final Graphics2D g2d = copy.createGraphics();
		g2d.drawImage(image, transform, null);
		g2d.dispose();

		return copy;
	}

}
